import { PropType, defineComponent, ref } from 'vue';
import { getTouchIdentifier, getControlPosition, createCoreData, addUserSelectStyles,
    addEvent, removeEvent, removeUserSelectStyles, snapToGrid } from './utils';

export interface DraggableCoreProps {
    handle?: string,
    grid?: number[]
}
export default defineComponent({
    name: 'DraggableCore',
    props: {
        onMouseDown: {type: Function},
        onStart: {type: Function},
        onStop: {type: Function},
        onDrag: {type: Function},
        allowAnyClick: {type: Boolean},
        disabled: {type: Boolean},
        handle: {type: String as PropType<DraggableCoreProps['handle']>},
        cancel: {type: String},
        scale: {type: Number},
        offsetParent: {type: Object},
        grid: {type: Array as PropType<DraggableCoreProps['grid']>}
    },
    setup (props, {slots}) {
        const touchIdentifier = ref(null);
        const lastX = ref(NaN);
        const lastY = ref(NaN);
        const dragging = ref(false);

        let thisNode: any;
        const handleDragStart = (e: any) => {
            props.onMouseDown && props.onMouseDown(e);

            // Only accept left-clicks.
            if (!props.allowAnyClick && typeof e.button === 'number' && e.button !== 0) return false;
            if (!thisNode || !thisNode.ownerDocument || !thisNode.ownerDocument.body) {
                throw new Error('<DraggableCore> not mounted on DragStart!');
            }

            const {ownerDocument} = thisNode;

            // Short circuit if handle or cancel prop was provided and selector doesn't match.
            if (props.disabled ||
                (!(e.target instanceof ownerDocument.defaultView.Node)) ||
                (props.handle && !document.querySelector(props.handle).contains(e.target)) ||
                (props.cancel && document.querySelector(props.cancel).contains(e.target))) {
                return;
            }

            // Prevent scrolling on mobile devices, like ipad/iphone.
            // Important that this is after handle/cancel.
            if (e.type === 'touchstart') e.preventDefault();

            // Set touch identifier in component state if this is a touch event. This allows us to
            // distinguish between individual touches on multitouch screens by identifying which
            // touchpoint was set to this element.
            const ti = getTouchIdentifier(e);
            touchIdentifier.value = ti;

            // Get the current drag point from the event. This is used as the offset.
            const position = getControlPosition(e, ti, props, thisNode);
            if (position == null) return; // not possible but satisfies flow
            const {x, y} = position;

            // Create an event object with all the data parents need to make a decision here.
            const coreEvent = createCoreData(thisNode, lastX.value, lastY.value, x, y);

            const shouldUpdate = props.onStart && props.onStart(e, coreEvent);
            if (shouldUpdate === false) return;

            // Add a style to the body to disable user-select. This prevents text from
            // being selected all over the page.
            addUserSelectStyles(ownerDocument);

            dragging.value = true;
            lastX.value = x;
            lastY.value = y;

            addEvent(ownerDocument, 'mousemove', handleDrag);
            addEvent(ownerDocument, 'mouseup', handleDragStop);
        };

        const handleDrag = (e: any) => {
            const position = getControlPosition(e, touchIdentifier.value, props, thisNode);
            if (position == null) return;
            let {x, y} = position;

            // Snap to grid if prop has been provided
            if (Array.isArray(props.grid)) {
                let deltaX = x - lastX.value, deltaY = y - lastY.value;
                [deltaX, deltaY] = snapToGrid(props.grid, deltaX, deltaY);
                if (!deltaX && !deltaY) return; // skip useless drag
                x = lastX.value + deltaX;
                y = lastY.value + deltaY;
            }

            const coreEvent = createCoreData(thisNode, lastX.value, lastY.value, x, y);

            const shouldUpdate = props.onDrag(e, coreEvent);
            if (shouldUpdate === false) {
                try {
                    // $FlowIgnore
                    handleDragStop(new MouseEvent('mouseup'));
                } catch (err) {
                    // Old browsers
                    const event = document.createEvent('MouseEvents');
                    // I see why this insanity was deprecated
                    // $FlowIgnore
                    event.initMouseEvent('mouseup', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
                    handleDragStop(event);
                }
                return;
            }

            lastX.value = x;
            lastY.value = y;
        };

        const handleDragStop = (e: any) => {
            if (!dragging.value) return;

            const position = getControlPosition(e, touchIdentifier.value, props, thisNode);
            if (position == null) return;
            const {x, y} = position;
            const coreEvent = createCoreData(thisNode, lastX.value, lastY.value, x, y);

            // Call event handler
            const shouldContinue = props.onStop(e, coreEvent);
            if (shouldContinue === false) return false;

            if (thisNode) {
                // Remove user-select hack
                removeUserSelectStyles(thisNode.ownerDocument);
            }

            // Reset the el.
            dragging.value = false;
            lastX.value = NaN;
            lastY.value = NaN;

            if (thisNode) {
                // Remove event handlers
                removeEvent(thisNode.ownerDocument, 'mousemove', handleDrag);
                removeEvent(thisNode.ownerDocument, 'mouseup', handleDragStop);
            }
        };

        const onMouseDown = (e: any) => {
            return handleDragStart(e);
        };

        const onMouseUp = (e: any) => {
            return handleDragStop(e);
        };

        return () => <div onMousedown={onMouseDown} onMouseup={onMouseUp} ref={(el) => thisNode = el}>
            {slots.default?.()}
        </div>;
    }
});
